package com.ikingtech.framework.sdk.web.support.handler;

import com.ikingtech.framework.sdk.context.exception.FrameworkException;
import com.ikingtech.framework.sdk.utils.Tools;
import com.ikingtech.framework.sdk.web.annotation.Encrypted;
import com.ikingtech.framework.sdk.web.properties.WebProperties;
import lombok.RequiredArgsConstructor;
import org.jsoup.Jsoup;
import org.jsoup.safety.Safelist;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.lang.NonNull;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter;

import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import static com.ikingtech.framework.sdk.context.constant.SecurityConstants.HEADER_PASSWORD_ENCRYPT;

/**
 * @author tie yan
 */
@RestControllerAdvice
@RequiredArgsConstructor
public class GlobalRequestAdvice extends RequestBodyAdviceAdapter {

    private final WebProperties properties;

    private static final List<String> XSS_REQUEST_WHITE_LIST = List.of(
            "/application/page/**"
    );

    @Override
    public boolean supports(@NonNull MethodParameter methodParameter, @NonNull Type targetType, @NonNull Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    /**
     * 在HTTP请求体读取后执行的逻辑。
     * 主要功能包括：请求体内容的格式化、安全检查以及解密处理。
     *
     * @param body 读取到的HTTP请求体内容。
     * @param inputMessage HTTP输入消息，包含请求头和请求体等信息。
     * @param parameter 方法参数信息，用于标识当前处理的方法参数。
     * @param targetType 目标类型，即期望将请求体转换成的Java类型。
     * @param converterType 用于转换HTTP请求体的转换器类型。
     * @return 经过处理的请求体对象。如果需要，会将请求体解密并转换成相应的Java对象。
     */
    @NonNull
    @Override
    public Object afterBodyRead(@NonNull Object body, @NonNull HttpInputMessage inputMessage, @NonNull MethodParameter parameter, @NonNull Type targetType, @NonNull Class<? extends HttpMessageConverter<?>> converterType) {
        String requestBodyStr = Tools.Str.EMPTY;
        // 根据请求头的Content-Type，将请求体转换成字符串形式
        if (MediaType.APPLICATION_JSON.isCompatibleWith(inputMessage.getHeaders().getContentType())) {
            requestBodyStr = Tools.Json.toJsonStr(body);
        }
        if (MediaType.TEXT_PLAIN.isCompatibleWith(inputMessage.getHeaders().getContentType())) {
            requestBodyStr = String.valueOf(body);
        }
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        // 检查请求体内容是否合法（防止XSS攻击）
        if (null != requestAttributes &&
                !Tools.Http.pathMatches(requestAttributes.getRequest().getRequestURI(), XSS_REQUEST_WHITE_LIST) &&
                !Tools.Http.pathMatches(requestAttributes.getRequest().getRequestURI(), this.properties.getXssIgnore()) &&
                !Jsoup.isValid(requestBodyStr, Safelist.relaxed())) {
            throw new FrameworkException("invalid request content(xss)");
        }

        // 检查是否需要对请求体进行解密
        String decryptRequired = inputMessage.getHeaders().getFirst(HEADER_PASSWORD_ENCRYPT);
        if (Tools.Str.isNotBlank(decryptRequired)) {
            Encrypted encryptedAnnotation = parameter.getParameterAnnotation(Encrypted.class);
            // 如果存在@Encrypted注解，则对请求体进行解密并转换成相应的Java对象
            if (null != encryptedAnnotation) {
                return Objects.requireNonNull(Tools.Json.toBean(this.decode(requestBodyStr, encryptedAnnotation.fields()), body.getClass()));
            }
        }
        return body;
    }

    private String decode(String originBody, String[] encryptedFields) {
        Map<String, Object> paramMap = Tools.Json.toMap(originBody);
        Map<String, Object> result = Tools.Json.toMap(originBody);
        // 获取请求密码并解密
        for (String encryptedField : encryptedFields) {
            if (paramMap.containsKey(encryptedField)) {
                result.put(encryptedField, Tools.Decrypt.instance("AES/CBC/PKCS7PADDING").aes((String) paramMap.get(encryptedField), "1234567890000000", "1234567890000000"));
            }
        }
        return Tools.Json.toJsonStr(result);
    }
}
